using System;
using System.Collections.Generic;
using System.Xml.Serialization;
using Kotonoha.Server.Core.Utils;
using NHibernate;
using NHibernate.Linq;
using Kotonoha.Server.Extensions;
using System.Linq;

namespace Kotonoha.Objects.SuperMemo {

    public class OFMatrixHolder {
        public static readonly OFMatrix Matrix = new OFMatrix();
    }

    public class OFMatrix : IDisposable {
        private ISession session;
        private Dictionary<int, Dictionary<double, OFMatrixItem>> dict = new Dictionary<int, Dictionary<double, OFMatrixItem>>();
        private Random random = new Random(Environment.TickCount % 32545123);

        public OFMatrix() {
            session = HibernateUtil.Model.Session;
            LoadMatrixItems();
        }

        private void LoadMatrixItems() {
            session.Linq<OFMatrixItem>().ForEach(Process);
        }

        private void Process(OFMatrixItem data) {
            Dictionary<double, OFMatrixItem> d;
            if (!dict.TryGetValue(data.N, out d)) {
                d = new Dictionary<double, OFMatrixItem>();
                dict.Add(data.N, d);
            }

            d[data.EF] = data;
        }

        private OFMatrixItem Get(int n, double ef) {
            ef = Math.Round(ef, 1);
            Dictionary<double, OFMatrixItem> d;
            if (!dict.TryGetValue(n, out d)) {
                d = new Dictionary<double, OFMatrixItem>();
                dict.Add(n, d);
            }

            OFMatrixItem it;
            if (!d.TryGetValue(ef, out it)) {
                it = new OFMatrixItem {Value = ef, N = n, EF = ef};
                session.Save(it);
                d.Add(ef, it);
            }
            return it;
        }

        public void Update(ItemLearningData data, double q, DateTime time) {
            double oldEf = data.Difficulty;
            double ef = Math.Max(1.3, oldEf + (0.1 - (5 - q)*(0.08 + (5 - q)*0.02)));
            data.Difficulty = ef;

            if (data.Repetition == 0) {
                data.Repetition = 1;
                data.IntervalLength = 4;
                UpdateMatrix(data, oldEf, q, 1, 1);
                data.IntervalLength = Get(1, ef).Value * CalculateRandomModifier();
                FinishData(data, time);
                return;
            }

            UpdateMatrix(data, oldEf, q, data.Repetition, data.Repetition == 1 ? 1 : data.Repetition - 1);
            UpdateData(data, q);
            FinishData(data, time);
            return;
        }

        private void UpdateMatrix(ItemLearningData data, double oldEf, double q, int n, int oldN) {
            double il = data.IntervalLength;
            var OF = Get(n, data.Difficulty);
            var oldOf = Get(oldN, oldEf);
            double mod5 = Math.Max(1.05, (il + 1)/il);
            double mod2 = Math.Min(0.75, (il - 1)/il);
            double mod;
            if (q > 4) {
                mod = 1 + (mod5 - 1)*(q - 4);
            } else {
                mod = 1 - (mod2 - 1)/2*(4 - q);
            }

            double oldOfVal = OF.Value;
            double newOf = oldOf.Value*mod;
            bool change = (q > 4 && newOf > oldOfVal) || (q < 4 && newOf < oldOfVal);
            //We change values if answer was bad and OF dropped or if answer was good and OF grew.
            if (change) {
                OF.Value = Math.Max(1.2, oldOfVal*0.9 + newOf*0.1);
                session.Update(OF);
            }
        }
        
        private void UpdateData(ItemLearningData data, double q) {
            double raw;
            if (q < 3.5) {
                ++data.Lapse;
                data.Repetition = 1;
                raw = Get(1, data.Difficulty).Value;
            } else {
                ++data.Repetition;
                raw = data.IntervalLength*Get(data.Repetition, data.Difficulty).Value;
            }
            data.IntervalLength = raw*CalculateRandomModifier();
        }

        private double CalculateRandomModifier() {
            const double a = 0.047;
            const double b = 0.092;
            double p = random.NextDouble() - 0.5;
            double m = -1/b*Math.Log(1 - b/a*Math.Abs(p));
            m = m*Math.Sign(p);
            return (100 + m)/100;
        }

        private void FinishData(ItemLearningData data, DateTime time) {
            data.IntervalStart = time;
            data.IntervalEnd = time.AddDays(data.IntervalLength);
            ISession s = HibernateUtil.Model.Session; //Yep, we don't use local session, it's only for Matrix items.
            s.Update(data); //Don't use transaction here, one should be already
        }

        public void Dispose() {
            session.Close();
        }

        public List<OFMatrixItem> Dump() {
            return dict.SelectMany(p => p.Value).Select(p => p.Value).ToList();
        }
    }

    public class OFMatrixItem : IEquatable<OFMatrixItem> {
        [XmlAttribute]
        public int N { get; set; }
        [XmlAttribute]
        public double EF { get; set; }
        [XmlAttribute]
        public double Value { get; set; }

        public bool Equals(OFMatrixItem other) {
            if (ReferenceEquals(null, other)) {
                return false;
            }
            if (ReferenceEquals(this, other)) {
                return true;
            }
            return other.N == N && other.EF == EF;
        }

        public override bool Equals(object obj) {
            if (ReferenceEquals(null, obj)) {
                return false;
            }
            if (ReferenceEquals(this, obj)) {
                return true;
            }
            if (obj.GetType() != typeof (OFMatrixItem)) {
                return false;
            }
            return Equals((OFMatrixItem) obj);
        }

        public override int GetHashCode() {
            unchecked {
                return (N*397) ^ EF.GetHashCode();
            }
        }
    }
}